package cn.akwangl.socket.nio;

import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.github.javafaker.Faker;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

/**
 * 生成测试数据到当前工程target/test 目录中.
 *
 * @author 阿坤同学
 * @date 2022/6/19 21:43
 * @since 0.0.6
 */
@Slf4j
public class CreateTestData {

    private static long _1M = 1048576;

    private static Faker faker = new Faker(Locale.CHINA);

    private static final int CORE_POOL_SIZE = 6;
    private static final int MAX_POOL_SIZE = 12;
    /**
     * 单位10M
     */
    private static final int DEFAULT_TEST_DATA_SIZE = 10;

    /**
     * W:工作时间
     * C:休息时间
     * Ncpu:线程核心数
     * Ucpu: CPU利用率
     * <p>
     * CPU 型: (1+W/C) * Ncpu * Ucpu(cpu利用率)
     * IO 型: (W+C)/C * Ncpu * Ucpu(cpu利用率)
     */
    private static ExecutorService executorService;

    public CreateTestData(ExecutorService executorService) {
        this.executorService = executorService;
    }

    /**
     * 随机生成指定大小的测试文件.
     *
     * @param size   大小 单位M
     * @param target 目标文件
     * @author 阿坤同学
     * @date 2022/6/20 23:13
     * @since 0.0.6
     */
    public void randomShardGenerates(long size, File target) {
        shardGenerates(size, target, true);
    }

    /**
     * 使用相同测试数据生成测试文件.
     *
     * @param size   大小 单位M
     * @param target 目标文件
     * @author 阿坤同学
     * @date 2022/6/20 23:14
     * @since 0.0.6
     */
    public void shardGenerates(long size, File target) {
        shardGenerates(size, target, false);
    }

    /**
     * 生成指定大小测试数据到指定File中.<br>
     * 采用线程池生成,分片数为线程池最小核心数,当random为true时则采用随机创建中文,这是一个非常耗时的过程,如果想快速的生成文件则将random设置为
     * false,它会利用第一次分片生成的数据写入文件.
     *
     * @param size   大小 单位M
     * @param target 目标文件
     * @param random true:随机创建测试数据,false:使用相同测试数据
     * @author 阿坤同学
     * @date 2022/6/19 21:52
     * @since 0.0.6
     */
    private void shardGenerates(long size, File target, boolean random) {
        FileOutputStream fos = null;
        try {
            fileInit(target, true);
            fos = new FileOutputStream(target, true);

            // M->byte
            size = size * _1M;
            List<Sharding> shardingList = Sharding.getSharding(target, size, random);
            AtomicInteger success = new AtomicInteger(shardingList.size());

            List<String> shardingFilePaths = new ArrayList();
            for (Sharding sharding : shardingList) {
                sharding.setSuccess(success);
                String shardingFileAbsolutePath = sharding.getShardingFile().getAbsolutePath();
                executorService.submit(sharding);
                shardingFilePaths.add(shardingFileAbsolutePath);
            }

            while (success.get() != 0) {
                // 等待线程池任务结束
            }
            String shardingFilePathsJson = Sharding.toJSONString(shardingFilePaths);
            fos.write(shardingFilePathsJson.getBytes(StandardCharsets.UTF_8));
        } catch (FileNotFoundException e) {
            log.error("找不到文件", e);
        } catch (IOException e) {
            log.error("数据写入异常", e);
        } finally {
            try {
                fos.close();
            } catch (IOException e) {
                log.error("文件输出流关闭失败", e);
            }
        }
    }

    /**
     * 生成指定size 大小的测试数据.<br/>
     * 注意这个方法时间复杂度为O(n^2),十分耗时。如果随机没有什么要求,就可以存一份,一直往文件里面写.
     *
     * @param size 单位M
     * @return 对应size M字节
     * @author 阿坤同学
     * @date 2022/6/20 20:50
     * @since 0.0.6
     */
    public static byte[] getTestData(double size) {
        if (size <= 0) {
            throw new RuntimeException("size <= 0");
        }
        if (Integer.MAX_VALUE < size) {
            throw new RuntimeException("超出最大支持范围");
        }

        int byteSize = (int) (_1M * size);
        byte[] result = new byte[byteSize];

        int shardingFileSize = byteSize / CORE_POOL_SIZE;
        // 任务数量
        int taskNum = byteSize / shardingFileSize;
        AtomicInteger success = new AtomicInteger(taskNum);
        int destPos = 0;
        int shardingSize = shardingFileSize;
        for (int i = 0; i < taskNum; i++) {
            GenerateTestDataTask testData = new GenerateTestDataTask(destPos, shardingSize, result, success);
            executorService.submit(testData);

            // 计算下一次数据段
            destPos = shardingSize + 1;
            shardingSize += shardingSize;
        }

        while (success.get() != 0) {
            // 等待线程池任务结束
        }
        return result;
    }


    /**
     * 文件初始化.
     *
     * @param file    文件
     * @param isCover 是否覆盖
     * @author 阿坤同学
     * @date 2022/6/20 12:42
     * @since 0.0.6
     */
    public static void fileInit(File file, boolean isCover) {
        // 创建文件上一级目录
        File fileParent = file.getParentFile();
        try {
            if (fileParent.exists()) {
                // 创建文件
                if (file.exists()) {
                    if (isCover) {
                        file.delete();
                        file.createNewFile();
                    }
                } else {
                    file.createNewFile();
                }
            } else {
                // 创建目录
                fileParent.mkdirs();
                // 创建文件
                file.createNewFile();
            }
        } catch (IOException e) {
            log.error("文件初始化失败", e);
        }
    }

    /**
     * 合并文件.
     *
     * @param source 源文件
     * @param out    输出
     * @author 阿坤同学
     * @date 2022/6/21 8:41
     * @since 0.0.6
     */
    public void mergeFile(File source, File out) {
        if (!source.exists()) {
            throw new RuntimeException("source 不存在");
        }

        if (!source.isFile()) {
            throw new RuntimeException("source 不可解析.");
        }

        fileInit(out, true);
        StringBuilder jsonBuilder = new StringBuilder();

        BufferedReader bufferedReader = null;
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(out, true);

            // 读取 分片地址
            bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(source), StandardCharsets.UTF_8));
            char[] chars = new char[8192];
            int length;
            while ((length = bufferedReader.read(chars)) != -1) {
                jsonBuilder.append(chars, 0, length);
            }
            List<String> shardingList = Sharding.parseObject(jsonBuilder.toString());
            if (shardingList == null || shardingList.isEmpty()) {
                throw new RuntimeException("文件已被损坏,解析失败");
            }

            // 分片合并
            for (String shardingFilePath : shardingList) {
                merge(shardingFilePath, outputStream);
            }
        } catch (FileNotFoundException e) {
            // ignore
        } catch (IOException e) {
            log.error("source 读取失败", e);
        } catch (JSONException e) {
            throw new RuntimeException("文件已被损坏,解析失败");
        } finally {
            try {
                bufferedReader.close();
                outputStream.close();
            } catch (IOException e) {
                log.error("资源关闭异常", e);
            }
        }
    }

    /**
     * 将分片文件合并并输出到outputStream中,outputStream关闭由上层调用控制.
     *
     * @param shardingFilePath 分片文件目录
     * @param outputStream     文件输出流
     * @author 阿坤同学
     * @date 2022/6/21 12:19
     * @since 0.0.6
     */
    private void merge(String shardingFilePath, OutputStream outputStream) {
        File sharding = new File(shardingFilePath);
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(sharding);

            int length;
            byte[] bytes = new byte[8192];
            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }
        } catch (FileNotFoundException e) {
            log.error("合并文件失败", e);
        } catch (IOException e) {
            log.error("合并文件失败", e);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("关闭资源失败", e);
            }
        }
    }

    /**
     * 分片文件.
     *
     * @author 阿坤同学
     * @date 2022/6/20 12:40
     * @since 0.0.6
     */
    @Data
    static class Sharding implements Runnable {
        /**
         * 分片文件,分片名称由源文件名称+自然数组成
         */
        private File shardingFile;
        /**
         * 分片大小
         */
        private long shardingSize;

        /**
         * 计数
         */
        private AtomicInteger success;

        /**
         * 生成测试数据
         */
        private Function<Long, byte[]> createTestDataFunction;

        /**
         * 获取分片文件.
         *
         * @param sourceFile   源文件
         * @param testFileSize 生成文件的大小 byte单位
         * @param random       true:随机创建测试数据,false:使用相同测试数据
         * @return 已初始化分片集合.
         * @author 阿坤同学
         * @date 2022/6/20 12:39
         */
        public static List<Sharding> getSharding(File sourceFile, long testFileSize, boolean random) {
            long shardingFileSize = testFileSize / CORE_POOL_SIZE;
            List<Sharding> result = new ArrayList();
            String absolutePath = sourceFile.getAbsolutePath();

            Function<Long, byte[]> createTestDataFunction;
            if (random) {
                // 字节转M
                double shardingSizeM = shardingFileSize / 1024d / 1024d;
                createTestDataFunction = (shardingSize) -> getTestData(shardingSizeM);
            } else {
                byte[] testData = getTestData(DEFAULT_TEST_DATA_SIZE);
                createTestDataFunction = (shardingSize) -> testData;
            }

            long residue = testFileSize;
            int number = 0;
            while (residue > 0) {
                if ((residue - shardingFileSize) > 0) {
                    residue = residue - shardingFileSize;
                } else {
                    shardingFileSize = residue;
                    residue = -1;
                }

                File shardingFile = new File(absolutePath + "-" + number);
                fileInit(shardingFile, true);
                Sharding sharding = new Sharding();
                sharding.setShardingFile(shardingFile);
                sharding.setShardingSize(shardingFileSize);
                sharding.setCreateTestDataFunction(createTestDataFunction);
                result.add(sharding);
                number++;
            }
            return result;
        }

        public static String toJSONString(List<String> shardingFilePaths) {
            return JSONObject.toJSONString(shardingFilePaths);
        }

        public static List<String> parseObject(String shardingJson) {
            return JSONObject.parseObject(shardingJson, ArrayList.class);
        }

        @Override
        public void run() {
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(shardingFile, true);
                while (shardingFile.length() < shardingSize) {
                    byte[] testData = createTestDataFunction.apply(getShardingSize());

                    long writeLength = shardingSize - shardingFile.length();

                    int length;
                    if (writeLength < testData.length) {
                        length = (int) writeLength;
                    } else {
                        length = testData.length;
                    }
                    fos.write(testData, 0, length);
                }
            } catch (FileNotFoundException e) {
                log.error("找不到文件", e);
            } catch (IOException e) {
                log.error("数据写入异常", e);
            } finally {
                try {
                    fos.close();
                } catch (IOException e) {
                    log.error("文件输出流关闭失败", e);
                }
                // 不管成功与否都递减
                success.getAndDecrement();
            }
        }
    }

    /**
     * 生成测试数据.
     *
     * @author 阿坤同学
     * @date 2022/6/21 14:26
     * @since 0.0.6
     */
    static class GenerateTestDataTask implements Runnable {

        int destPos;
        int shardingSize;
        byte[] bytes;
        AtomicInteger success;

        public GenerateTestDataTask(int destPos, int shardingSize, byte[] bytes, AtomicInteger success) {
            this.destPos = destPos;
            this.shardingSize = shardingSize;
            this.bytes = bytes;
            this.success = success;
        }

        @Override
        public void run() {
            while (destPos < bytes.length) {
                String name = faker.name().name();
                byte[] temp = name.getBytes(StandardCharsets.UTF_8);
                int copyLength = temp.length;

                int freeSpaceLength = bytes.length - destPos;
                if (freeSpaceLength < copyLength) {
                    copyLength = freeSpaceLength;
                }
                System.arraycopy(temp, 0, bytes, destPos, copyLength);
                destPos += copyLength;
            }

            success.getAndDecrement();
        }
    }

    public static void main(String[] args) {
        String basePath = System.getProperty("user.dir") + "\\rocketmq\\target\\test";

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, 20, TimeUnit.SECONDS, new ArrayBlockingQueue(200));
        CreateTestData createTestData = new CreateTestData(threadPoolExecutor);

        StopWatch stopWatch = new StopWatch("生成测试数据性能测试");
        File file1 = new File(basePath + "\\a.txt");
        File file2 = new File(basePath + "\\b.txt");
        File file3 = new File(basePath + "\\c.txt");
        File out1 = new File(basePath + "\\a-merge.txt");
        File out2 = new File(basePath + "\\b-merge.txt");
        File out3 = new File(basePath + "\\c-merge.txt");
        stopWatch.start("生成10M");
        createTestData.shardGenerates(10, file1);
        createTestData.mergeFile(file1, out1);
        stopWatch.stop();

        stopWatch.start("生成512M");
        createTestData.shardGenerates(512, file2);
        createTestData.mergeFile(file2, out2);
        stopWatch.stop();

        stopWatch.start("生成1G");
        createTestData.shardGenerates(1024, file3);
        createTestData.mergeFile(file3, out3);
        stopWatch.stop();

        threadPoolExecutor.shutdown();
        System.out.println(stopWatch.prettyPrint());
        System.out.println(stopWatch.getTotalTimeSeconds());
    }
}
